mirror of
https://github.com/the1812/Bilibili-Evolved.git
synced 2025-11-04 21:22:45 +08:00
157 lines
4.5 KiB
TypeScript
157 lines
4.5 KiB
TypeScript
import { DownloadPackage, PackageEntry } from '@/core/download'
|
||
import { meta } from '@/core/meta'
|
||
import { getComponentSettings } from '@/core/settings'
|
||
import { Toast } from '@/core/toast'
|
||
import { formatFileSize, formatPercent } from '@/core/utils/formatters'
|
||
import { title as pluginTitle } from '.'
|
||
import type { Options } from '../../../../components/video/download'
|
||
import { DownloadVideoAction } from '../../../../components/video/download/types'
|
||
import { FFmpeg } from './ffmpeg'
|
||
import { getCacheOrFetch, httpGet, toastProgress, toBlobUrl } from './utils'
|
||
|
||
const ffmpeg = new FFmpeg()
|
||
|
||
async function loadFFmpeg() {
|
||
const toast = Toast.info('正在加载 FFmpeg', `${pluginTitle} - 初始化`)
|
||
|
||
const progress = toastProgress(toast)
|
||
const [worker, core, wasm] = await Promise.all([
|
||
getCacheOrFetch(
|
||
'ffmpeg-worker',
|
||
meta.compilationInfo.altCdn.library.ffmpeg.worker,
|
||
progress(0, '正在加载 FFmpeg Worker'),
|
||
),
|
||
getCacheOrFetch(
|
||
'ffmpeg-core',
|
||
meta.compilationInfo.altCdn.library.ffmpeg.core,
|
||
progress(1, '正在加载 FFmpeg Core'),
|
||
),
|
||
getCacheOrFetch(
|
||
'ffmpeg-wasm',
|
||
meta.compilationInfo.altCdn.library.ffmpeg.wasm,
|
||
progress(2, '正在加载 FFmpeg WASM'),
|
||
),
|
||
])
|
||
|
||
await ffmpeg.load({
|
||
workerLoadURL: toBlobUrl(worker, 'text/javascript'),
|
||
coreURL: toBlobUrl(core, 'text/javascript'),
|
||
wasmURL: toBlobUrl(wasm, 'application/wasm'),
|
||
})
|
||
|
||
toast.message = '完成!'
|
||
toast.close()
|
||
}
|
||
|
||
async function single(
|
||
name: string,
|
||
videoUrl: string,
|
||
audioUrl: string,
|
||
ffmetadata: string,
|
||
outputMkv: boolean,
|
||
pageIndex = 1,
|
||
totalPages = 1,
|
||
) {
|
||
const toast = Toast.info('', `${pluginTitle} - ${pageIndex} / ${totalPages}`)
|
||
|
||
const progress = toastProgress(toast)
|
||
const [video, audio] = await Promise.all([
|
||
httpGet(videoUrl, progress(0, '正在下载视频流')),
|
||
httpGet(audioUrl, progress(1, '正在下载音频流')),
|
||
])
|
||
|
||
await ffmpeg.writeFile('video', video)
|
||
await ffmpeg.writeFile('audio', audio)
|
||
|
||
const args = ['-i', 'video', '-i', 'audio']
|
||
|
||
if (ffmetadata) {
|
||
await ffmpeg.writeFile('ffmetadata', new TextEncoder().encode(ffmetadata))
|
||
args.push('-i', 'ffmetadata', '-map_metadata', '2')
|
||
if (!outputMkv) {
|
||
args.push('-movflags', '+use_metadata_tags')
|
||
}
|
||
}
|
||
|
||
args.push('-codec', 'copy', '-f', outputMkv ? 'matroska' : 'mp4', 'output')
|
||
|
||
console.debug('FFmpeg commandline args:', args.join(' '))
|
||
|
||
ffmpeg.onProgress(event => {
|
||
toast.message = `混流中: ${formatPercent(event.progress)}`
|
||
})
|
||
await ffmpeg.exec(args)
|
||
|
||
const output = await ffmpeg.readFile('output')
|
||
const outputBlob = new Blob([output], {
|
||
type: outputMkv ? 'video/x-matroska' : 'video/mp4',
|
||
})
|
||
|
||
toast.message = '完成!'
|
||
toast.duration = 1000
|
||
|
||
await Promise.all([
|
||
ffmpeg.deleteFile('video'),
|
||
ffmpeg.deleteFile('audio'),
|
||
ffmpeg.deleteFile('output'),
|
||
ffmetadata ? ffmpeg.deleteFile('ffmetadata') : Promise.resolve(),
|
||
])
|
||
|
||
await DownloadPackage.single(
|
||
name.replace(/.[^/.]+$/, `.${outputMkv ? 'mkv' : 'mp4'}`),
|
||
outputBlob,
|
||
)
|
||
}
|
||
|
||
export async function run(action: DownloadVideoAction, muxWithMetadata: boolean) {
|
||
if (!ffmpeg.loaded) {
|
||
await loadFFmpeg()
|
||
}
|
||
|
||
const { infos: pages, extraAssets } = action
|
||
|
||
let ffmetadata: PackageEntry[]
|
||
if (muxWithMetadata) {
|
||
const extraAssetsForBrowser = []
|
||
for (const { asset, instance } of extraAssets) {
|
||
if (!ffmetadata && asset.name === 'saveVideoMetadata' && instance.type === 'ffmetadata') {
|
||
ffmetadata = await asset.getAssets(pages, instance)
|
||
} else {
|
||
extraAssetsForBrowser.push({ asset, instance })
|
||
}
|
||
}
|
||
action.extraAssets = extraAssetsForBrowser
|
||
}
|
||
|
||
const { dashAudioExtension, dashFlacAudioExtension, dashVideoExtension } =
|
||
getComponentSettings<Options>('downloadVideo').options
|
||
|
||
for (let i = 0; i < pages.length; i++) {
|
||
const page = pages[i]
|
||
const [video, audio] = page.titledFragments
|
||
if (
|
||
!(
|
||
page.fragments.length === 2 &&
|
||
video.extension === dashVideoExtension &&
|
||
(audio.extension === dashAudioExtension || audio.extension === dashFlacAudioExtension)
|
||
)
|
||
) {
|
||
throw new Error('仅支持 DASH 格式视频和音频')
|
||
}
|
||
|
||
if (video.size + audio.size > 4294967295) {
|
||
throw new Error(`仅支持合并 4GB 内的音视频(${formatFileSize(video.size + audio.size)})`)
|
||
}
|
||
|
||
await single(
|
||
video.title,
|
||
video.url,
|
||
audio.url,
|
||
<string>ffmetadata?.[i]?.data,
|
||
audio.extension === dashFlacAudioExtension,
|
||
i + 1,
|
||
pages.length,
|
||
)
|
||
}
|
||
}
|