// ==UserScript== // @name 视频格式互转工具 // @namespace https://workbuddy.cn/video-converter // @version 1.0.0 // @description 浏览器内视频格式互转,支持 MP4/MOV/AVI/WMV/FLV/MKV/3GP/RMVB/WebM/MPEG/VOB/DAT 等数十种格式互转,基于 FFmpeg.wasm // @author WorkBuddy // @match *://*/* // @grant GM_addStyle // @run-at document-idle // @noframes // @license MIT // ==/UserScript== (function () { 'use strict'; /* ============================================================ * 配置常量 * ============================================================ */ const FFMPEG_VER = '0.12.10'; const FFMPEG_UTIL_VER = '0.12.1'; const FFMPEG_CORE_VER = '0.12.6'; const CDN = 'https://unpkg.com'; /* ============================================================ * 格式定义 —— 按类别分组 * ============================================================ */ const FORMAT_CATEGORIES = [ { group: '常见在线流媒体', formats: [ { ext: 'mp4', name: 'MP4', desc: 'MPEG-4 Part 14,最通用' }, { ext: 'flv', name: 'FLV', desc: 'Flash Video' }, { ext: 'f4v', name: 'F4V', desc: 'Flash MP4 Video' }, { ext: 'webm', name: 'WebM', desc: 'Google WebM' }, ], }, { group: '移动设备格式', formats: [ { ext: 'm4v', name: 'M4V', desc: 'iTunes Video' }, { ext: 'mov', name: 'MOV', desc: 'QuickTime Movie' }, { ext: '3gp', name: '3GP', desc: '3GPP 多媒体' }, { ext: '3g2', name: '3G2', desc: '3GPP2 多媒体' }, ], }, { group: 'RealPlayer', formats: [ { ext: 'rm', name: 'RM', desc: 'RealMedia' }, { ext: 'rmvb', name: 'RMVB', desc: 'RealMedia 可变码率' }, ], }, { group: '微软格式', formats: [ { ext: 'wmv', name: 'WMV', desc: 'Windows Media Video' }, { ext: 'avi', name: 'AVI', desc: 'Audio Video Interleave' }, { ext: 'asf', name: 'ASF', desc: 'Advanced Systems Format' }, ], }, { group: 'MPEG 视频', formats: [ { ext: 'mpg', name: 'MPG', desc: 'MPEG-1 Video' }, { ext: 'mpeg', name: 'MPEG', desc: 'MPEG Video' }, { ext: 'mpe', name: 'MPE', desc: 'MPEG Video' }, { ext: 'ts', name: 'TS', desc: 'MPEG Transport Stream' }, ], }, { group: 'DV 格式', formats: [ { ext: 'div', name: 'DIV', desc: 'DIVX Video' }, { ext: 'dv', name: 'DV', desc: 'Digital Video' }, { ext: 'divx', name: 'DIVX', desc: 'DivX Video' }, ], }, { group: '其他格式', formats: [ { ext: 'vob', name: 'VOB', desc: 'DVD Video Object' }, { ext: 'dat', name: 'DAT', desc: 'VCD Video' }, { ext: 'mkv', name: 'MKV', desc: 'Matroska Video' }, { ext: 'lavf', name: 'LAVF', desc: 'Libavformat' }, { ext: 'cpk', name: 'CPK', desc: 'CPK Video' }, { ext: 'dirac', name: 'Dirac', desc: 'Dirac Video' }, { ext: 'ram', name: 'RAM', desc: 'RealMedia RAM' }, { ext: 'qt', name: 'QT', desc: 'QuickTime' }, { ext: 'fli', name: 'FLI', desc: 'FLIC Animation' }, { ext: 'flc', name: 'FLC', desc: 'FLIC Animation' }, { ext: 'mod', name: 'MOD', desc: 'MOD Video' }, ], }, ]; /* 所有可用扩展名集合(用于文件选择 accept 属性) */ const ALL_EXTS = []; FORMAT_CATEGORIES.forEach((cat) => cat.formats.forEach((f) => ALL_EXTS.push(f.ext))); /* ============================================================ * 编解码器映射 —— 每种输出格式对应的 FFmpeg 编解码参数 * ============================================================ */ const CODEC_MAP = { // 常见在线流媒体 mp4: { v: 'libx264', a: 'aac', extra: ['-movflags', '+faststart'] }, flv: { v: 'libx264', a: 'aac' }, f4v: { v: 'libx264', a: 'aac', extra: ['-movflags', '+faststart'] }, webm: { v: 'libvpx', a: 'libvorbis' }, // 移动设备格式 m4v: { v: 'libx264', a: 'aac', extra: ['-movflags', '+faststart'] }, mov: { v: 'libx264', a: 'aac', extra: ['-movflags', '+faststart'] }, '3gp': { v: 'h263', a: 'aac' }, '3g2': { v: 'h263', a: 'aac' }, // RealPlayer rm: { v: 'libx264', a: 'aac', note: 'RM 为容器封装,Real 专用编码器不可用' }, rmvb: { v: 'libx264', a: 'aac', note: 'RMVB 为容器封装,Real 专用编码器不可用' }, // 微软格式 wmv: { v: 'msmpeg4v2', a: 'mp3' }, avi: { v: 'mpeg4', a: 'mp3' }, asf: { v: 'msmpeg4v2', a: 'mp3' }, // MPEG 视频 mpg: { v: 'mpeg1video', a: 'mp2' }, mpeg: { v: 'mpeg1video', a: 'mp2' }, mpe: { v: 'mpeg1video', a: 'mp2' }, ts: { v: 'libx264', a: 'aac', extra: ['-f', 'mpegts'] }, // DV 格式 div: { v: 'libx264', a: 'aac' }, dv: { v: 'dvvideo', a: 'pcm_s16le' }, divx: { v: 'libx264', a: 'aac' }, // 其他格式 vob: { v: 'mpeg2video', a: 'mp2' }, dat: { v: 'mpeg1video', a: 'mp2' }, mkv: { v: 'libx264', a: 'aac' }, lavf: { v: 'libx264', a: 'aac' }, cpk: { v: 'libx264', a: 'aac', note: 'CPK 格式支持有限' }, dirac: { v: 'libx264', a: 'aac' }, ram: { v: 'libx264', a: 'aac', note: 'RAM 为容器封装' }, qt: { v: 'libx264', a: 'aac', extra: ['-movflags', '+faststart'] }, fli: { v: 'libx264', a: 'aac' }, flc: { v: 'libx264', a: 'aac' }, mod: { v: 'libx264', a: 'aac' }, }; /* 质量预设 */ const QUALITY_PRESETS = [ { id: 'high', name: '高质量', crf: '18', scale: '' }, { id: 'standard', name: '标准', crf: '23', scale: '' }, { id: 'fast', name: '快速', crf: '28', scale: '' }, { id: '720p', name: '720p', crf: '23', scale: '-2:720' }, { id: '480p', name: '480p', crf: '23', scale: '-2:480' }, { id: '360p', name: '360p', crf: '23', scale: '-2:360' }, ]; /* ============================================================ * CSS 样式 * ============================================================ */ const CSS = ` .vfc-btn { position: fixed; bottom: 28px; right: 28px; z-index: 2147483646; width: 56px; height: 56px; border-radius: 50%; cursor: pointer; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); box-shadow: 0 4px 20px rgba(102,126,234,.45); display: flex; align-items: center; justify-content: center; transition: transform .25s ease, box-shadow .25s ease; animation: vfc-pulse 3s ease-in-out infinite; } .vfc-btn:hover { transform: scale(1.1); box-shadow: 0 6px 28px rgba(102,126,234,.6); } .vfc-btn:active { transform: scale(.95); } .vfc-btn svg { width: 26px; height: 26px; fill: #fff; } @keyframes vfc-pulse { 0%,100%{box-shadow:0 4px 20px rgba(102,126,234,.45)} 50%{box-shadow:0 4px 28px rgba(102,126,234,.65)} } .vfc-overlay { position: fixed; inset: 0; z-index: 2147483647; background: rgba(10,10,30,.72); backdrop-filter: blur(8px); display: none; align-items: center; justify-content: center; animation: vfc-fade .25s ease; } .vfc-overlay.active { display: flex; } @keyframes vfc-fade { from{opacity:0} to{opacity:1} } .vfc-modal { width: 92%; max-width: 620px; max-height: 88vh; overflow-y: auto; background: linear-gradient(160deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%); border: 1px solid rgba(255,255,255,.1); border-radius: 20px; box-shadow: 0 24px 80px rgba(0,0,0,.5); animation: vfc-slide .3s cubic-bezier(.16,1,.3,1); scrollbar-width: thin; scrollbar-color: rgba(255,255,255,.2) transparent; } .vfc-modal::-webkit-scrollbar { width: 6px; } .vfc-modal::-webkit-scrollbar-thumb { background: rgba(255,255,255,.2); border-radius: 3px; } @keyframes vfc-slide { from{transform:translateY(40px) scale(.96);opacity:0} to{transform:translateY(0) scale(1);opacity:1} } .vfc-header { display: flex; align-items: center; justify-content: space-between; padding: 20px 24px; border-bottom: 1px solid rgba(255,255,255,.08); position: sticky; top: 0; z-index: 10; background: linear-gradient(160deg, #1a1a2e 0%, #16213e 100%); border-radius: 20px 20px 0 0; } .vfc-header h2 { margin: 0; font-size: 20px; color: #fff; font-weight: 700; display: flex; align-items: center; gap: 8px; } .vfc-header .vfc-logo { font-size: 24px; } .vfc-close { width: 34px; height: 34px; border-radius: 10px; border: none; cursor: pointer; background: rgba(255,255,255,.08); color: #fff; font-size: 18px; display: flex; align-items: center; justify-content: center; transition: background .2s; line-height: 1; } .vfc-close:hover { background: rgba(255,80,80,.3); } .vfc-body { padding: 24px; } .vfc-section { margin-bottom: 22px; } .vfc-section-title { font-size: 13px; color: rgba(255,255,255,.5); text-transform: uppercase; letter-spacing: 1px; margin-bottom: 10px; font-weight: 600; } /* 文件拖放区域 */ .vfc-drop { border: 2px dashed rgba(255,255,255,.15); border-radius: 14px; padding: 36px 20px; text-align: center; cursor: pointer; transition: all .25s; position: relative; } .vfc-drop:hover { border-color: rgba(102,126,234,.6); background: rgba(102,126,234,.06); } .vfc-drop.drag-over { border-color: #667eea; background: rgba(102,126,234,.12); transform: scale(1.01); } .vfc-drop svg { width: 40px; height: 40px; fill: rgba(255,255,255,.3); margin-bottom: 12px; } .vfc-drop p { margin: 4px 0; color: rgba(255,255,255,.6); font-size: 14px; } .vfc-drop .vfc-browse { color: #667eea; font-weight: 600; text-decoration: underline; } .vfc-drop input[type=file] { display: none; } .vfc-file-info { display: none; align-items: center; gap: 12px; padding: 14px 16px; background: rgba(255,255,255,.05); border-radius: 12px; margin-top: 12px; } .vfc-file-info.show { display: flex; } .vfc-file-icon { width: 40px; height: 40px; border-radius: 10px; background: linear-gradient(135deg,#667eea,#764ba2); display: flex; align-items: center; justify-content: center; flex-shrink: 0; } .vfc-file-icon svg { width: 22px; height: 22px; fill: #fff; } .vfc-file-meta { flex: 1; min-width: 0; } .vfc-file-name { color: #fff; font-size: 14px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .vfc-file-detail { color: rgba(255,255,255,.45); font-size: 12px; margin-top: 2px; } .vfc-file-clear { width: 28px; height: 28px; border-radius: 8px; border: none; cursor: pointer; background: rgba(255,255,255,.08); color: rgba(255,255,255,.6); font-size: 15px; flex-shrink: 0; } .vfc-file-clear:hover { background: rgba(255,80,80,.25); color: #fff; } /* 视频预览 */ .vfc-preview { display: none; margin-top: 12px; border-radius: 12px; overflow: hidden; background: #000; } .vfc-preview.show { display: block; } .vfc-preview video { width: 100%; max-height: 200px; display: block; } /* 格式选择网格 */ .vfc-format-group { margin-bottom: 14px; } .vfc-group-label { font-size: 12px; color: rgba(255,255,255,.4); margin-bottom: 6px; font-weight: 500; } .vfc-format-grid { display: flex; flex-wrap: wrap; gap: 8px; } .vfc-format-chip { padding: 8px 16px; border-radius: 10px; border: 1.5px solid rgba(255,255,255,.1); background: rgba(255,255,255,.04); color: rgba(255,255,255,.7); cursor: pointer; font-size: 13px; font-weight: 600; transition: all .2s; user-select: none; position: relative; } .vfc-format-chip:hover { border-color: rgba(102,126,234,.5); color: #fff; background: rgba(102,126,234,.1); } .vfc-format-chip.selected { border-color: #667eea; background: linear-gradient(135deg, rgba(102,126,234,.3), rgba(118,75,162,.3)); color: #fff; box-shadow: 0 2px 12px rgba(102,126,234,.3); } .vfc-format-chip.selected::after { content: '\\2713'; position: absolute; top: -6px; right: -6px; width: 16px; height: 16px; border-radius: 50%; background: #667eea; color: #fff; font-size: 10px; display: flex; align-items: center; justify-content: center; } /* 质量选择 */ .vfc-quality-row { display: flex; flex-wrap: wrap; gap: 8px; } .vfc-quality-chip { padding: 7px 14px; border-radius: 8px; border: 1.5px solid rgba(255,255,255,.1); background: rgba(255,255,255,.04); color: rgba(255,255,255,.6); cursor: pointer; font-size: 12px; font-weight: 500; transition: all .2s; } .vfc-quality-chip:hover { border-color: rgba(102,126,234,.4); color: #fff; } .vfc-quality-chip.selected { border-color: #667eea; background: rgba(102,126,234,.2); color: #fff; } /* 转换按钮 */ .vfc-convert-btn { width: 100%; padding: 15px; border-radius: 14px; border: none; cursor: pointer; font-size: 16px; font-weight: 700; color: #fff; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); transition: all .25s; margin-top: 4px; } .vfc-convert-btn:hover:not(:disabled) { transform: translateY(-2px); box-shadow: 0 8px 30px rgba(102,126,234,.4); } .vfc-convert-btn:active:not(:disabled) { transform: translateY(0); } .vfc-convert-btn:disabled { opacity: .4; cursor: not-allowed; } .vfc-convert-btn.converting { background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); } /* 进度条 */ .vfc-progress-wrap { display: none; margin-top: 16px; } .vfc-progress-wrap.show { display: block; } .vfc-progress-bar { width: 100%; height: 8px; background: rgba(255,255,255,.1); border-radius: 4px; overflow: hidden; } .vfc-progress-fill { height: 100%; border-radius: 4px; width: 0%; background: linear-gradient(90deg, #667eea, #764ba2, #f093fb); background-size: 200% 100%; animation: vfc-shimmer 1.5s linear infinite; transition: width .4s ease; } @keyframes vfc-shimmer { from{background-position:200% 0} to{background-position:-200% 0} } .vfc-progress-text { display: flex; justify-content: space-between; margin-top: 8px; font-size: 12px; color: rgba(255,255,255,.5); } /* 日志 */ .vfc-log-wrap { display: none; margin-top: 12px; } .vfc-log-wrap.show { display: block; } .vfc-log-toggle { font-size: 12px; color: rgba(255,255,255,.4); cursor: pointer; user-select: none; } .vfc-log-toggle:hover { color: rgba(255,255,255,.7); } .vfc-log { margin-top: 6px; max-height: 120px; overflow-y: auto; padding: 10px 12px; background: rgba(0,0,0,.3); border-radius: 8px; font-family: 'Consolas','Monaco',monospace; font-size: 11px; color: rgba(255,255,255,.4); line-height: 1.6; display: none; } .vfc-log.show { display: block; } .vfc-log::-webkit-scrollbar { width: 4px; } .vfc-log::-webkit-scrollbar-thumb { background: rgba(255,255,255,.15); border-radius: 2px; } /* 结果区域 */ .vfc-result { display: none; margin-top: 16px; padding: 18px; border-radius: 14px; background: rgba(102,234,170,.08); border: 1px solid rgba(102,234,170,.2); } .vfc-result.show { display: block; animation: vfc-fade .4s ease; } .vfc-result-title { color: #6ee7b7; font-size: 14px; font-weight: 600; margin-bottom: 10px; display: flex; align-items: center; gap: 6px; } .vfc-result-info { display: flex; gap: 16px; margin-bottom: 12px; font-size: 12px; color: rgba(255,255,255,.5); } .vfc-result-info span { display: flex; flex-direction: column; } .vfc-result-info span strong { color: #fff; font-size: 15px; margin-top: 2px; } .vfc-download-btn { display: inline-flex; align-items: center; gap: 8px; padding: 11px 24px; border-radius: 10px; border: none; cursor: pointer; font-size: 14px; font-weight: 600; color: #fff; background: linear-gradient(135deg, #10b981, #059669); transition: all .2s; } .vfc-download-btn:hover { transform: translateY(-2px); box-shadow: 0 6px 20px rgba(16,185,129,.35); } .vfc-download-btn svg { width: 18px; height: 18px; fill: #fff; } /* 状态消息 */ .vfc-status { font-size: 13px; color: rgba(255,255,255,.6); text-align: center; margin-top: 12px; min-height: 18px; } .vfc-status.error { color: #f5576c; } .vfc-status.success { color: #6ee7b7; } .vfc-status.loading { color: #93c5fd; } /* 响应式 */ @media (max-width: 600px) { .vfc-btn { width: 48px; height: 48px; bottom: 18px; right: 18px; } .vfc-btn svg { width: 22px; height: 22px; } .vfc-modal { width: 96%; max-height: 92vh; } .vfc-body { padding: 16px; } .vfc-format-grid { gap: 6px; } .vfc-format-chip { padding: 6px 12px; font-size: 12px; } } `; /* ============================================================ * 工具函数 * ============================================================ */ function formatSize(bytes) { if (bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; } function formatTime(seconds) { if (!seconds || isNaN(seconds)) return '--'; const h = Math.floor(seconds / 3600); const m = Math.floor((seconds % 3600) / 60); const s = Math.floor(seconds % 60); if (h > 0) return `${h}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`; return `${m}:${String(s).padStart(2, '0')}`; } function getExt(filename) { const parts = filename.split('.'); return parts.length > 1 ? parts.pop().toLowerCase() : ''; } function getBaseName(filename) { return filename.replace(/\.[^/.]+$/, ''); } function getMimeType(ext) { const map = { mp4: 'video/mp4', mov: 'video/quicktime', avi: 'video/x-msvideo', wmv: 'video/x-ms-wmv', flv: 'video/x-flv', mkv: 'video/x-matroska', webm: 'video/webm', '3gp': 'video/3gpp', '3g2': 'video/3gpp2', m4v: 'video/x-m4v', mpg: 'video/mpeg', mpeg: 'video/mpeg', mpe: 'video/mpeg', ts: 'video/mp2t', vob: 'video/dvd', dat: 'video/mpeg', rm: 'application/vnd.rn-realmedia', rmvb: 'application/vnd.rn-realmedia-vbr', asf: 'video/x-ms-asf', f4v: 'video/x-f4v', ogg: 'video/ogg', dv: 'video/dv', }; return map[ext] || 'video/mp4'; } function loadScript(src) { return new Promise((resolve, reject) => { const s = document.createElement('script'); s.src = src; s.onload = () => resolve(); s.onerror = () => reject(new Error('脚本加载失败: ' + src)); document.head.appendChild(s); }); } /* ============================================================ * FFmpeg 加载(懒加载,首次转换时才加载) * ============================================================ */ let ffmpegInstance = null; let ffmpegLoading = null; async function initFFmpeg(onStatus) { if (ffmpegInstance) return ffmpegInstance; if (ffmpegLoading) return ffmpegLoading; ffmpegLoading = (async () => { onStatus('正在加载 FFmpeg 库文件...'); await loadScript(`${CDN}/@ffmpeg/ffmpeg@${FFMPEG_VER}/dist/umd/ffmpeg.js`); await loadScript(`${CDN}/@ffmpeg/util@${FFMPEG_UTIL_VER}/dist/umd/index.js`); if (!window.FFmpegWASM || !window.FFmpegUtil) { throw new Error('FFmpeg 库加载失败,请检查网络连接'); } const { FFmpeg } = window.FFmpegWASM; const { toBlobURL } = window.FFmpegUtil; const ffmpeg = new FFmpeg(); const baseURL = `${CDN}/@ffmpeg/core@${FFMPEG_CORE_VER}/dist/umd`; onStatus('正在下载 FFmpeg WASM 核心(约 30MB,请耐心等待)...'); const coreURL = await toBlobURL(`${baseURL}/ffmpeg-core.js`, 'text/javascript'); const wasmURL = await toBlobURL(`${baseURL}/ffmpeg-core.wasm`, 'application/wasm'); onStatus('正在初始化 FFmpeg 引擎...'); await ffmpeg.load({ coreURL, wasmURL }); ffmpegInstance = ffmpeg; onStatus('FFmpeg 引擎就绪'); return ffmpeg; })(); try { return await ffmpegLoading; } catch (e) { ffmpegLoading = null; throw e; } } /* ============================================================ * 转换逻辑 * ============================================================ */ function buildFFmpegArgs(inputName, outputName, targetExt, qualityId) { const codec = CODEC_MAP[targetExt] || { v: 'libx264', a: 'aac' }; const quality = QUALITY_PRESETS.find((q) => q.id === qualityId) || QUALITY_PRESETS[1]; const args = ['-i', inputName]; // 视频编码器 args.push('-c:v', codec.v); if (codec.v === 'libx264') { args.push('-preset', 'ultrafast', '-crf', quality.crf); } // 音频编码器 args.push('-c:a', codec.a); // 分辨率缩放 if (quality.scale) { args.push('-vf', `scale=${quality.scale}`); } // 格式特定参数 if (codec.extra) { args.push(...codec.extra); } // 输出文件 args.push(outputName); return args; } async function convertVideo(file, targetExt, qualityId, onProgress, onLog, onStatus) { const ffmpeg = await initFFmpeg(onStatus); const { fetchFile } = window.FFmpegUtil; const inputExt = getExt(file.name) || 'mp4'; const inputName = `input_${Date.now()}.${inputExt}`; const outputName = `output_${Date.now()}.${targetExt}`; // 写入输入文件 onStatus('正在读取文件...'); await ffmpeg.writeFile(inputName, await fetchFile(file)); // 构建转换参数 const args = buildFFmpegArgs(inputName, outputName, targetExt, qualityId); // 进度回调 const progressHandler = ({ progress }) => { if (progress > 0 && progress <= 1) { onProgress(Math.round(progress * 100)); } }; ffmpeg.on('progress', progressHandler); // 日志回调 const logHandler = ({ message }) => onLog(message); ffmpeg.on('log', logHandler); // 执行转换 onStatus('正在转换视频格式...'); let success = false; try { await ffmpeg.exec(args); success = true; } catch (e) { // 主编解码器失败,尝试回退方案 onStatus('主编解码器失败,尝试兼容方案...'); onLog('>>> 尝试回退方案: libx264 + aac'); const fallbackArgs = ['-i', inputName, '-c:v', 'libx264', '-preset', 'ultrafast', '-crf', '23', '-c:a', 'aac', outputName]; try { await ffmpeg.exec(fallbackArgs); success = true; } catch (e2) { // 再尝试流拷贝 onLog('>>> 尝试流拷贝方案'); const copyArgs = ['-i', inputName, '-c', 'copy', outputName]; try { await ffmpeg.exec(copyArgs); success = true; } catch (e3) { throw new Error('所有转换方案均失败: ' + (e3.message || e3)); } } } finally { ffmpeg.off('progress', progressHandler); ffmpeg.off('log', logHandler); } if (!success) throw new Error('转换失败'); // 读取输出文件 onStatus('正在生成输出文件...'); const data = await ffmpeg.readFile(outputName); const blob = new Blob([data.buffer], { type: getMimeType(targetExt) }); // 清理虚拟文件系统 try { await ffmpeg.deleteFile(inputName); } catch (_) {} try { await ffmpeg.deleteFile(outputName); } catch (_) {} return blob; } /* ============================================================ * UI 创建 * ============================================================ */ let selectedFile = null; let selectedFormat = ''; let selectedQuality = 'standard'; function createUI() { GM_addStyle(CSS); // 浮动按钮 const btn = document.createElement('div'); btn.className = 'vfc-btn'; btn.id = 'vfc-btn'; btn.title = '视频格式互转工具'; btn.innerHTML = ``; btn.addEventListener('click', openModal); document.body.appendChild(btn); // 遮罩 + 模态框 const overlay = document.createElement('div'); overlay.className = 'vfc-overlay'; overlay.id = 'vfc-overlay'; overlay.innerHTML = buildModalHTML(); document.body.appendChild(overlay); // 点击遮罩关闭 overlay.addEventListener('click', (e) => { if (e.target === overlay) closeModal(); }); bindEvents(); } function buildModalHTML() { // 生成格式选择网格 const formatHTML = FORMAT_CATEGORIES.map((cat) => `
拖放视频文件到此处,或 点击选择
支持 ${ALL_EXTS.length} 种格式:${ALL_EXTS.join('、')}