重命名 RPC 预设:
@@ -69,17 +73,19 @@
diff --git a/registry/lib/plugins/video/download/wasm-output/handler.ts b/registry/lib/plugins/video/download/wasm-output/handler.ts
index 1c48e12fd..7d6842e1c 100644
--- a/registry/lib/plugins/video/download/wasm-output/handler.ts
+++ b/registry/lib/plugins/video/download/wasm-output/handler.ts
@@ -1,4 +1,4 @@
-import { DownloadPackage } from '@/core/download'
+import { DownloadPackage, PackageEntry } from '@/core/download'
import { meta } from '@/core/meta'
import { getComponentSettings } from '@/core/settings'
import { Toast } from '@/core/toast'
@@ -6,38 +6,38 @@ import { title as pluginTitle } from '.'
import type { Options } from '../../../../components/video/download'
import { DownloadVideoAction } from '../../../../components/video/download/types'
import { FFmpeg } from './ffmpeg'
-import { getCacheOrGet, httpGet, toastProgress, toBlobUrl } from './utils'
+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(
- await getCacheOrGet(
- 'ffmpeg-worker',
- meta.compilationInfo.altCdn.library.ffmpeg.worker,
- toastProgress(toast, '正在加载 FFmpeg Worker'),
- ),
- 'text/javascript',
- ),
- coreURL: toBlobUrl(
- await getCacheOrGet(
- 'ffmpeg-core',
- meta.compilationInfo.altCdn.library.ffmpeg.core,
- toastProgress(toast, '正在加载 FFmpeg Core'),
- ),
- 'text/javascript',
- ),
- wasmURL: toBlobUrl(
- await getCacheOrGet(
- 'ffmpeg-wasm',
- meta.compilationInfo.altCdn.library.ffmpeg.wasm,
- toastProgress(toast, '正在加载 FFmpeg WASM'),
- ),
- 'application/wasm',
- ),
+ workerLoadURL: toBlobUrl(worker, 'text/javascript'),
+ coreURL: toBlobUrl(core, 'text/javascript'),
+ wasmURL: toBlobUrl(wasm, 'application/wasm'),
})
+
toast.message = '完成!'
toast.close()
}
@@ -46,52 +46,76 @@ async function single(
name: string,
videoUrl: string,
audioUrl: string,
- isFlac: boolean,
+ ffmetadata: string,
+ outputMkv: boolean,
pageIndex = 1,
totalPages = 1,
) {
const toast = Toast.info('', `${pluginTitle} - ${pageIndex} / ${totalPages}`)
- ffmpeg.writeFile('video', await httpGet(videoUrl, toastProgress(toast, '正在下载视频流')))
- ffmpeg.writeFile('audio', await httpGet(audioUrl, toastProgress(toast, '正在下载音频流')))
-
- toast.message = '混流中……'
- const outputExt = isFlac ? 'mkv' : 'mp4'
- name = name.replace(/.[^/.]+$/, `.${outputExt}`)
- await ffmpeg.exec([
- '-i',
- 'video',
- '-i',
- 'audio',
- '-c:v',
- 'copy',
- '-c:a',
- 'copy',
- '-f',
- isFlac ? 'matroska' : 'mp4',
- `output.${outputExt}`,
+ const progress = toastProgress(toast)
+ const [video, audio] = await Promise.all([
+ httpGet(videoUrl, progress(0, '正在下载视频流')),
+ httpGet(audioUrl, progress(1, '正在下载音频流')),
])
- const output = await ffmpeg.readFile(`output.${outputExt}`)
+ ffmpeg.writeFile('video', video)
+ ffmpeg.writeFile('audio', audio)
+
+ const args = ['-i', 'video', '-i', 'audio']
+
+ if (ffmetadata) {
+ 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(' '))
+
+ toast.message = '混流中……'
+ await ffmpeg.exec(args)
+
+ const output = await ffmpeg.readFile('output')
const outputBlob = new Blob([output], {
- type: isFlac ? 'video/x-matroska' : 'video/mp4',
+ type: outputMkv ? 'video/x-matroska' : 'video/mp4',
})
toast.message = '完成!'
toast.duration = 1000
- await DownloadPackage.single(name, outputBlob)
+ await DownloadPackage.single(
+ name.replace(/.[^/.]+$/, `.${outputMkv ? 'mkv' : 'mp4'}`),
+ outputBlob,
+ )
}
-export async function run(action: DownloadVideoAction) {
+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
('downloadVideo').options
- const pages = action.infos
for (let i = 0; i < pages.length; i++) {
const page = pages[i]
const [video, audio] = page.titledFragments
@@ -109,6 +133,7 @@ export async function run(action: DownloadVideoAction) {
video.title,
video.url,
audio.url,
+ ffmetadata?.[i]?.data,
audio.extension === dashFlacAudioExtension,
i + 1,
pages.length,
diff --git a/registry/lib/plugins/video/download/wasm-output/index.ts b/registry/lib/plugins/video/download/wasm-output/index.ts
index 3cde08f64..d5039f259 100644
--- a/registry/lib/plugins/video/download/wasm-output/index.ts
+++ b/registry/lib/plugins/video/download/wasm-output/index.ts
@@ -19,14 +19,15 @@ export const plugin: PluginMetadata = {
outputs.push({
name: 'wasm',
displayName: 'WASM',
- description: `${desc},运行过程中请勿关闭页面,初次使用或清除缓存后需要加载约 30 MB 的 WASM 文件`,
- runAction: async action => {
+ description: `${desc}。运行过程中请勿关闭页面,初次使用或清除缓存后需要加载约 30 MB 的 WASM 文件。`,
+ runAction: async (action, instance) => {
try {
- await run(action)
+ await run(action, instance.muxWithMetadata)
} catch (error) {
Toast.error(String(error), title)
}
},
+ component: () => import('./Config.vue').then(m => m.default),
})
})
},
diff --git a/registry/lib/plugins/video/download/wasm-output/utils.ts b/registry/lib/plugins/video/download/wasm-output/utils.ts
index 165e3559d..fe2ec33d7 100644
--- a/registry/lib/plugins/video/download/wasm-output/utils.ts
+++ b/registry/lib/plugins/video/download/wasm-output/utils.ts
@@ -1,14 +1,23 @@
import { Toast } from '@/core/toast'
import { formatFileSize, formatPercent } from '@/core/utils/formatters'
import { getOrLoad, storeNames } from './database'
+import { RuntimeLibraryDefinition, RuntimeLibrary } from '@/core/runtime-library'
-type OnProgress = (received: number, length: number) => void
+type OnProgress = (received: number, total: number) => void
-export function toastProgress(toast: Toast, message: string): OnProgress {
- return (r, l) => {
- toast.message = `${message}: ${formatFileSize(r)}${
- l > 0 ? ` / ${formatFileSize(l)} @ ${formatPercent(r / l)}` : ''
- }`
+function formatProgress(received: number, total: number) {
+ return `${formatFileSize(received)}${
+ total > 0 ? ` / ${formatFileSize(total)} @ ${formatPercent(received / total)}` : ''
+ }`
+}
+
+export function toastProgress(toast: Toast) {
+ const lines = []
+ return (line: number, message: string): OnProgress => {
+ return (r, l) => {
+ lines[line] = `${message}: ${formatProgress(r, l)}`
+ toast.message = lines.join('\n')
+ }
}
}
@@ -48,8 +57,21 @@ export async function httpGet(url: string, onprogress: OnProgress) {
return chunksAll
}
-export async function getCacheOrGet(key: string, url: string, loading: OnProgress) {
- return getOrLoad(storeNames.cache, key, async () => httpGet(url, loading))
+export async function getCacheOrFetch(
+ key: string,
+ library: RuntimeLibraryDefinition,
+ loading: OnProgress,
+) {
+ return getOrLoad(storeNames.cache, key, async () => {
+ const content = await httpGet(library.url, loading)
+ const sha256 = await RuntimeLibrary.sha256(content)
+ if (sha256 !== library.sha256) {
+ throw new Error(
+ `Check integrity failed from ${library.url}, expected = ${library.sha256}, actual = ${sha256}`,
+ )
+ }
+ return content
+ })
}
export function toBlobUrl(buffer: Uint8Array, mimeType: string) {
diff --git a/src/client/common.meta.json b/src/client/common.meta.json
index 89090a9cc..f5a6c2d70 100644
--- a/src/client/common.meta.json
+++ b/src/client/common.meta.json
@@ -38,7 +38,7 @@
"*"
],
"require": [
- "[altCdn.library.lodash]"
+ "[altCdn.library.lodash.url]#sha256=[altCdn.library.lodash.sha256]"
],
"icon": "[altCdn.smallLogo]",
"icon64": "[altCdn.logo]"
diff --git a/src/components/SwitchOptions.vue b/src/components/SwitchOptions.vue
index 152b871fc..68fe1d161 100644
--- a/src/components/SwitchOptions.vue
+++ b/src/components/SwitchOptions.vue
@@ -5,7 +5,7 @@
{{ options.optionDisplayName }}
@@ -128,6 +128,7 @@ export default Vue.extend({
}
.switch-icon {
margin-right: 8px;
+ opacity: 0.75;
transform: scale(0.9);
}
.dim {
diff --git a/src/components/video/video-info.ts b/src/components/video/video-info.ts
index 43f5d3af7..9251d5689 100644
--- a/src/components/video/video-info.ts
+++ b/src/components/video/video-info.ts
@@ -1,5 +1,17 @@
import { getJsonWithCredentials, getText } from '@/core/ajax'
+export interface UpInfo {
+ uid: number
+ name: string
+ faceUrl: string
+}
+
+export interface VideoPageInfo {
+ cid: number
+ title: string
+ pageNumber: number
+}
+
export class VideoInfo {
aid: string
bvid: string
@@ -13,16 +25,8 @@ export class VideoInfo {
tagName: string
title: string
description: string
- up: {
- uid: number
- name: string
- faceUrl: string
- }
- pages: {
- cid: number
- title: string
- pageNumber: number
- }[]
+ up: UpInfo
+ pages: VideoPageInfo[]
constructor(id: string, bvid = false) {
if (bvid) {
diff --git a/src/core/runtime-library.ts b/src/core/runtime-library.ts
index 73f2a3140..4ba753410 100644
--- a/src/core/runtime-library.ts
+++ b/src/core/runtime-library.ts
@@ -5,26 +5,54 @@ import type StreamSaverType from 'streamsaver'
import { monkey } from './ajax'
import { meta } from './meta'
+export type RuntimeLibraryDefinition = { url: string; sha256: string }
export interface RuntimeLibraryConfig {
- url: string
+ library: RuntimeLibraryDefinition
getModule: (window: Window) => LibraryType
}
+
export class RuntimeLibrary implements PromiseLike {
private modulePromise: Promise
constructor(public config: RuntimeLibraryConfig) {}
+ static async sha256(content: string | BufferSource) {
+ const hashBuffer = await window.crypto.subtle.digest(
+ 'SHA-256',
+ typeof content === 'string' ? new TextEncoder().encode(content) : content,
+ )
+ const hashArray = Array.from(new Uint8Array(hashBuffer))
+ const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('')
+ return hashHex
+ }
+
+ private async checkIntegrity(content: string | BufferSource) {
+ const sha256 = await RuntimeLibrary.sha256(content)
+ if (sha256 !== this.config.library.sha256) {
+ throw new Error(
+ `[RuntimeLibrary] Check integrity failed from ${this.config.library.url}, expected = ${this.config.library.sha256}, actual = ${sha256}`,
+ )
+ }
+ console.log(
+ `[Runtime Library] Checked integrity from ${this.config.library.url}, hash = ${sha256}`,
+ )
+ }
+
async then(
resolve?: (value: LibraryType) => Resolve | PromiseLike,
reject?: (reason: any) => Reject | PromiseLike,
) {
try {
- const { url, getModule } = this.config
+ const {
+ library: { url },
+ getModule,
+ } = this.config
if (!this.modulePromise) {
this.modulePromise = (async () => {
console.log(`[Runtime Library] Start download from ${url}`)
const code: string = await monkey({ url })
- console.log(`[Runtime Library] Downloaded from ${url} , length = ${code.length}`)
+ console.log(`[Runtime Library] Downloaded from ${url}, length = ${code.length}`)
+ await this.checkIntegrity(code)
;(function runEval() {
return eval(code)
// eslint-disable-next-line no-extra-bind
@@ -41,18 +69,18 @@ export class RuntimeLibrary implements PromiseLike {
}
}
export const protobufLibrary = new RuntimeLibrary({
- url: meta.compilationInfo.altCdn.library.protobuf,
+ library: meta.compilationInfo.altCdn.library.protobuf,
getModule: window => window.protobuf,
})
export const JSZipLibrary = new RuntimeLibrary({
- url: meta.compilationInfo.altCdn.library.jszip,
+ library: meta.compilationInfo.altCdn.library.jszip,
getModule: window => window.JSZip,
})
export const SortableJSLibrary = new RuntimeLibrary({
- url: meta.compilationInfo.altCdn.library.sortable,
+ library: meta.compilationInfo.altCdn.library.sortable,
getModule: window => window.Sortable,
})
export const StreamSaverLibrary = new RuntimeLibrary({
- url: meta.compilationInfo.altCdn.library.streamsaver,
+ library: meta.compilationInfo.altCdn.library.streamsaver,
getModule: window => window.streamSaver,
})
diff --git a/webpack/cdn/github.ts b/webpack/cdn/github.ts
index 05872c69f..f23bf1994 100644
--- a/webpack/cdn/github.ts
+++ b/webpack/cdn/github.ts
@@ -10,12 +10,26 @@ export const github: CdnConfig = {
stableClient: `https://${host}/${owner}/Bilibili-Evolved/master/dist/bilibili-evolved.user.js`,
previewClient: `https://${host}/${owner}/Bilibili-Evolved/preview/dist/bilibili-evolved.preview.user.js`,
library: {
- lodash: `https://${host}/lodash/lodash/4.17.21/dist/lodash.min.js`,
- protobuf: `https://${host}/protobufjs/protobuf.js/v6.10.1/dist/light/protobuf.min.js`,
- jszip: `https://${host}/Stuk/jszip/v3.7.1/dist/jszip.min.js`,
- sortable: `https://${host}/SortableJS/Sortable/1.14.0/Sortable.min.js`,
- mdi: `https://${owner}.github.io/Bilibili-Evolved/static/mdi/mdi.css`,
- streamsaver: `https://${host}/jimmywarting/StreamSaver.js/2.0.6/StreamSaver.js`,
+ lodash: {
+ url: `https://${host}/lodash/lodash/4.17.21/dist/lodash.min.js`,
+ sha256: 'a9705dfc47c0763380d851ab1801be6f76019f6b67e40e9b873f8b4a0603f7a9',
+ },
+ protobuf: {
+ url: `https://${host}/protobufjs/protobuf.js/v6.10.1/dist/light/protobuf.min.js`,
+ sha256: '8978daf871b02d683ecaee371861702a6f31d0a4c52925b7db2bb1655a8bc7d1',
+ },
+ jszip: {
+ url: `https://${host}/Stuk/jszip/v3.7.1/dist/jszip.min.js`,
+ sha256: 'c9e4a52bac18aee4f3f90d05fbca603f5b0f5bf1ce8c45e60bb4ed3a2cb2ed86',
+ },
+ sortable: {
+ url: `https://${host}/SortableJS/Sortable/1.14.0/Sortable.min.js`,
+ sha256: '0ea5a6fbfbf5434b606878533cb7a66bcf700f0f08afe908335d0978fb63ad94',
+ },
+ streamsaver: {
+ url: `https://${host}/jimmywarting/StreamSaver.js/2.0.6/StreamSaver.js`,
+ sha256: 'a110f78e0b092481dc372901c4d57ae50681d773bc9d55e62356f9a22f17e24b',
+ },
// https://github.com/the1812/Bilibili-Evolved/pull/4521#discussion_r1402084486
ffmpeg: jsDelivr.library.ffmpeg,
},
diff --git a/webpack/cdn/jsdelivr.ts b/webpack/cdn/jsdelivr.ts
index 3765b9774..219c8ec27 100644
--- a/webpack/cdn/jsdelivr.ts
+++ b/webpack/cdn/jsdelivr.ts
@@ -9,16 +9,39 @@ export const jsDelivr: CdnConfig = {
stableClient: `https://${host}/gh/${owner}/Bilibili-Evolved@master/dist/bilibili-evolved.user.js`,
previewClient: `https://${host}/gh/${owner}/Bilibili-Evolved@preview/dist/bilibili-evolved.preview.user.js`,
library: {
- lodash: `https://${host}/npm/lodash@4.17.21/lodash.min.js`,
- protobuf: `https://${host}/npm/protobufjs@6.10.1/dist/light/protobuf.min.js`,
- jszip: `https://${host}/npm/jszip@3.7.1/dist/jszip.min.js`,
- sortable: `https://${host}/npm/sortablejs@1.14.0/Sortable.min.js`,
- mdi: `https://${host}/gh/${owner}/Bilibili-Evolved@master/docs/static/mdi/mdi.css`,
- streamsaver: `https://${host}/npm/streamsaver@2.0.6/StreamSaver.min.js`,
+ lodash: {
+ url: `https://${host}/npm/lodash@4.17.21/lodash.min.js`,
+ sha256: 'a9705dfc47c0763380d851ab1801be6f76019f6b67e40e9b873f8b4a0603f7a9',
+ },
+ protobuf: {
+ url: `https://${host}/npm/protobufjs@6.10.1/dist/light/protobuf.min.js`,
+ sha256: '8978daf871b02d683ecaee371861702a6f31d0a4c52925b7db2bb1655a8bc7d1',
+ },
+ jszip: {
+ url: `https://${host}/npm/jszip@3.7.1/dist/jszip.min.js`,
+ sha256: 'c9e4a52bac18aee4f3f90d05fbca603f5b0f5bf1ce8c45e60bb4ed3a2cb2ed86',
+ },
+ sortable: {
+ url: `https://${host}/npm/sortablejs@1.14.0/Sortable.min.js`,
+ sha256: '0ea5a6fbfbf5434b606878533cb7a66bcf700f0f08afe908335d0978fb63ad94',
+ },
+ streamsaver: {
+ url: `https://${host}/npm/streamsaver@2.0.6/StreamSaver.min.js`,
+ sha256: '64f465e51e5992be894c5d42330b781544eda5462069fe6be4c7421f02d28c92',
+ },
ffmpeg: {
- worker: `https://${host}/npm/@ffmpeg/ffmpeg@0.12.4/dist/umd/814.ffmpeg.js`,
- core: `https://${host}/npm/@ffmpeg/core@0.12.4/dist/umd/ffmpeg-core.js`,
- wasm: `https://${host}/npm/@ffmpeg/core@0.12.4/dist/umd/ffmpeg-core.wasm`,
+ worker: {
+ url: `https://${host}/npm/@ffmpeg/ffmpeg@0.12.4/dist/umd/814.ffmpeg.js`,
+ sha256: 'baf19437171b1bccae4416e4da69fb40455b8e67142f79c8ec9da36b1de7fd8a',
+ },
+ core: {
+ url: `https://${host}/npm/@ffmpeg/core@0.12.4/dist/umd/ffmpeg-core.js`,
+ sha256: '6af6b8cd8c878dec6f61f3cd6be16e88f9391dd265e51f20afea5c0f718bfba0',
+ },
+ wasm: {
+ url: `https://${host}/npm/@ffmpeg/core@0.12.4/dist/umd/ffmpeg-core.wasm`,
+ sha256: '925bd7ef35d4e0f715254cd650f4cfc68c4ec6ecebf293face72b92da904ddda',
+ },
},
},
smallLogo: `https://${host}/gh/${owner}/Bilibili-Evolved@preview/images/logo-small.png`,
diff --git a/webpack/cdn/types.ts b/webpack/cdn/types.ts
index 841d97d4d..5860ee74e 100644
--- a/webpack/cdn/types.ts
+++ b/webpack/cdn/types.ts
@@ -1,3 +1,4 @@
+export type ExternalLibrary = { url: string; sha256: string }
export interface CdnConfig {
name: string
owner: string
@@ -5,16 +6,15 @@ export interface CdnConfig {
stableClient: string
previewClient: string
library: {
- lodash: string
- protobuf: string
- jszip: string
- sortable: string
- mdi: string
- streamsaver: string
+ lodash: ExternalLibrary
+ protobuf: ExternalLibrary
+ jszip: ExternalLibrary
+ sortable: ExternalLibrary
+ streamsaver: ExternalLibrary
ffmpeg: {
- worker: string
- core: string
- wasm: string
+ worker: ExternalLibrary
+ core: ExternalLibrary
+ wasm: ExternalLibrary
}
}
smallLogo: string