refactor: remove FFmpeg dependencies and related video processing logic
- Deleted FFmpeg related packages from package.json and package-lock.json. - Removed video transcoding and editing functionalities from FileContent.vue. - Simplified video handling logic and error management in FileContent.vue. - Added a clear button in MilkdownEditor.vue for clearing the editor content. - Enhanced UniverPreview.vue to clear detached popups and mount nodes on destroy. - Updated docBlockPlugin.ts to improve context handling for document blocks. - Cleaned up vite.config.js by removing cross-origin isolation headers.
This commit is contained in:
@@ -1,230 +0,0 @@
|
||||
import coreURL from '@ffmpeg/core?url'
|
||||
import wasmURL from '@ffmpeg/core/wasm?url'
|
||||
import classWorkerURL from '@ffmpeg/ffmpeg/worker?url'
|
||||
|
||||
const VIDEO_EXTENSIONS = new Set([
|
||||
'mp4',
|
||||
'webm',
|
||||
'ogv',
|
||||
'ogg',
|
||||
'mov',
|
||||
'm4v',
|
||||
'avi',
|
||||
'mkv',
|
||||
'flv',
|
||||
'wmv',
|
||||
'3gp',
|
||||
'm2ts',
|
||||
'mts',
|
||||
'ts'
|
||||
])
|
||||
|
||||
const FORCED_TRANSCODE_EXTENSIONS = new Set([
|
||||
'3gp',
|
||||
'avi',
|
||||
'flv',
|
||||
'm2ts',
|
||||
'mkv',
|
||||
'mts',
|
||||
'ts',
|
||||
'wmv'
|
||||
])
|
||||
|
||||
const VIDEO_MIME_BY_EXTENSION = {
|
||||
m4v: 'video/x-m4v',
|
||||
mov: 'video/quicktime',
|
||||
mp4: 'video/mp4',
|
||||
ogg: 'video/ogg',
|
||||
ogv: 'video/ogg',
|
||||
webm: 'video/webm'
|
||||
}
|
||||
|
||||
let ffmpegInstancePromise = null
|
||||
let progressHandler = null
|
||||
let transcodeQueue = Promise.resolve()
|
||||
|
||||
function getFileExtension(name = '') {
|
||||
const parts = String(name).split('.')
|
||||
return parts.length > 1 ? parts.pop().toLowerCase() : ''
|
||||
}
|
||||
|
||||
function getNodeMimeType(node) {
|
||||
return String(node?.mimeType || '').toLowerCase()
|
||||
}
|
||||
|
||||
function formatFfmpegTime(seconds = 0) {
|
||||
const totalSeconds = Math.max(0, Number(seconds) || 0)
|
||||
return totalSeconds.toFixed(3)
|
||||
}
|
||||
|
||||
function createVideoProbe() {
|
||||
if (typeof document === 'undefined') return null
|
||||
return document.createElement('video')
|
||||
}
|
||||
|
||||
function toAbsoluteURL(url) {
|
||||
if (!url || typeof location === 'undefined') {
|
||||
return url
|
||||
}
|
||||
|
||||
return new URL(url, location.href).href
|
||||
}
|
||||
|
||||
function getLoadConfigs() {
|
||||
return [{
|
||||
classWorkerURL: toAbsoluteURL(classWorkerURL),
|
||||
coreURL: toAbsoluteURL(coreURL),
|
||||
wasmURL: toAbsoluteURL(wasmURL)
|
||||
}]
|
||||
}
|
||||
|
||||
async function loadFFmpegRuntime(FFmpeg, fetchFile) {
|
||||
let lastError = null
|
||||
|
||||
for (const loadConfig of getLoadConfigs()) {
|
||||
const ffmpeg = new FFmpeg()
|
||||
ffmpeg.on('progress', ({ progress }) => {
|
||||
if (typeof progressHandler === 'function') {
|
||||
progressHandler(Math.max(0, Math.min(1, progress || 0)))
|
||||
}
|
||||
})
|
||||
|
||||
try {
|
||||
await ffmpeg.load(loadConfig)
|
||||
return { ffmpeg, fetchFile }
|
||||
} catch (error) {
|
||||
lastError = error
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError || new Error('FFmpeg 初始化失败')
|
||||
}
|
||||
|
||||
async function getFFmpegInstance() {
|
||||
if (!ffmpegInstancePromise) {
|
||||
ffmpegInstancePromise = Promise.all([
|
||||
import('@ffmpeg/ffmpeg'),
|
||||
import('@ffmpeg/util')
|
||||
]).then(([{ FFmpeg }, { fetchFile }]) => loadFFmpegRuntime(FFmpeg, fetchFile))
|
||||
}
|
||||
|
||||
return ffmpegInstancePromise
|
||||
}
|
||||
|
||||
function buildMp4Command(inputName, outputName, options = {}) {
|
||||
const startTime = Math.max(0, Number(options.startTime) || 0)
|
||||
const endTime = Number.isFinite(options.endTime) ? Number(options.endTime) : null
|
||||
const duration = endTime === null ? null : Math.max(0.05, endTime - startTime)
|
||||
const args = []
|
||||
|
||||
if (startTime > 0) {
|
||||
args.push('-ss', formatFfmpegTime(startTime))
|
||||
}
|
||||
|
||||
args.push('-i', inputName)
|
||||
|
||||
if (duration !== null) {
|
||||
args.push('-t', formatFfmpegTime(duration))
|
||||
}
|
||||
|
||||
if (options.preferEncoding !== false) {
|
||||
args.push('-c:v', 'libx264', '-preset', 'ultrafast', '-pix_fmt', 'yuv420p')
|
||||
if (options.muteAudio) {
|
||||
args.push('-an')
|
||||
} else {
|
||||
args.push('-c:a', 'aac')
|
||||
}
|
||||
} else if (options.muteAudio) {
|
||||
args.push('-an')
|
||||
}
|
||||
|
||||
args.push('-movflags', '+faststart', '-y', outputName)
|
||||
return args
|
||||
}
|
||||
|
||||
async function executePreferredMp4Command(ffmpeg, inputName, outputName, options = {}) {
|
||||
try {
|
||||
await ffmpeg.exec(buildMp4Command(inputName, outputName, {
|
||||
...options,
|
||||
preferEncoding: true
|
||||
}))
|
||||
} catch {
|
||||
await ffmpeg.exec(buildMp4Command(inputName, outputName, {
|
||||
...options,
|
||||
preferEncoding: false
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
async function runMp4Job(blob, fileName, options = {}) {
|
||||
const { ffmpeg, fetchFile } = await getFFmpegInstance()
|
||||
const ext = getFileExtension(fileName) || 'bin'
|
||||
const inputName = `input-${Date.now()}.${ext}`
|
||||
const outputName = `output-${Date.now()}.mp4`
|
||||
progressHandler = typeof options.onProgress === 'function' ? options.onProgress : null
|
||||
|
||||
try {
|
||||
await ffmpeg.writeFile(inputName, await fetchFile(blob))
|
||||
await executePreferredMp4Command(ffmpeg, inputName, outputName, options)
|
||||
const data = await ffmpeg.readFile(outputName)
|
||||
return new Blob([data.buffer], { type: 'video/mp4' })
|
||||
} finally {
|
||||
progressHandler = null
|
||||
await ffmpeg.deleteFile?.(inputName)
|
||||
await ffmpeg.deleteFile?.(outputName)
|
||||
}
|
||||
}
|
||||
|
||||
export function isVideoFile(node) {
|
||||
const ext = getFileExtension(node?.name)
|
||||
const mime = getNodeMimeType(node)
|
||||
return mime.startsWith('video/') || VIDEO_EXTENSIONS.has(ext)
|
||||
}
|
||||
|
||||
export function canPlayVideoNatively(node) {
|
||||
if (!isVideoFile(node)) return false
|
||||
|
||||
const ext = getFileExtension(node?.name)
|
||||
if (FORCED_TRANSCODE_EXTENSIONS.has(ext)) return false
|
||||
|
||||
const mime = getNodeMimeType(node) || VIDEO_MIME_BY_EXTENSION[ext] || ''
|
||||
if (!mime) return ['mp4', 'ogv', 'ogg', 'webm'].includes(ext)
|
||||
|
||||
const video = createVideoProbe()
|
||||
if (!video) return ['mp4', 'ogv', 'ogg', 'webm'].includes(ext)
|
||||
return video.canPlayType(mime) !== ''
|
||||
}
|
||||
|
||||
export async function transcodeVideoToMp4(blob, fileName, options = {}) {
|
||||
const run = async () => {
|
||||
try {
|
||||
return await runMp4Job(blob, fileName, options)
|
||||
} catch (error) {
|
||||
const message = error instanceof Error && error.message
|
||||
? error.message
|
||||
: '浏览器内视频转换失败'
|
||||
throw new Error(message)
|
||||
}
|
||||
}
|
||||
|
||||
const result = transcodeQueue.then(run, run)
|
||||
transcodeQueue = result.then(() => undefined, () => undefined)
|
||||
return result
|
||||
}
|
||||
|
||||
export async function editVideoBlob(blob, fileName, options = {}) {
|
||||
const run = async () => {
|
||||
try {
|
||||
return await runMp4Job(blob, fileName, options)
|
||||
} catch (error) {
|
||||
const message = error instanceof Error && error.message
|
||||
? error.message
|
||||
: '浏览器内视频编辑失败'
|
||||
throw new Error(message)
|
||||
}
|
||||
}
|
||||
|
||||
const result = transcodeQueue.then(run, run)
|
||||
transcodeQueue = result.then(() => undefined, () => undefined)
|
||||
return result
|
||||
}
|
||||
Reference in New Issue
Block a user