From ba449f4681bbf4baa96b918dd71c2cb0394428ac Mon Sep 17 00:00:00 2001 From: hzj <1304805162@qq.com> Date: Mon, 27 Apr 2026 19:04:37 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E6=96=B0=E7=BB=84=E4=BB=B6):=20vap?= =?UTF-8?q?=E8=A7=86=E9=A2=91=E6=92=AD=E6=94=BE=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 1 + src/components/VapMp4Player.vue | 245 ++++++++++++++++++++++++++++++++ src/utils/vapMp4Config.js | 151 ++++++++++++++++++++ 3 files changed, 397 insertions(+) create mode 100644 src/components/VapMp4Player.vue create mode 100644 src/utils/vapMp4Config.js diff --git a/package.json b/package.json index 94f1ed3..bc8ea2e 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "pinia": "^3.0.3", "pinia-plugin-persistedstate": "^4.7.1", "vant": "^4.9.21", + "video-animation-player": "^1.0.5", "vue": "^3.5.18", "vue-i18n": "^11.1.12", "vue-router": "^4.5.1" diff --git a/src/components/VapMp4Player.vue b/src/components/VapMp4Player.vue new file mode 100644 index 0000000..431bb69 --- /dev/null +++ b/src/components/VapMp4Player.vue @@ -0,0 +1,245 @@ + + + + + diff --git a/src/utils/vapMp4Config.js b/src/utils/vapMp4Config.js new file mode 100644 index 0000000..80d5c10 --- /dev/null +++ b/src/utils/vapMp4Config.js @@ -0,0 +1,151 @@ +const VAP_CONFIG_BOX = 'vapc' +const TEXT_DECODER = typeof TextDecoder !== 'undefined' ? new TextDecoder('utf-8') : null + +const configCache = new Map() +const containerBoxes = new Set([ + 'moov', + 'trak', + 'mdia', + 'minf', + 'stbl', + 'edts', + 'udta', + 'meta', + 'ilst', + 'moof', + 'traf', + 'mvex', + 'mfra', +]) + +function readBoxType(view, offset) { + return String.fromCharCode( + view.getUint8(offset), + view.getUint8(offset + 1), + view.getUint8(offset + 2), + view.getUint8(offset + 3) + ) +} + +function readBoxSize(view, offset, end) { + const smallSize = view.getUint32(offset) + + if (smallSize === 0) { + return { size: end - offset, headerSize: 8 } + } + + if (smallSize === 1) { + const high = view.getUint32(offset + 8) + const low = view.getUint32(offset + 12) + return { size: high * 2 ** 32 + low, headerSize: 16 } + } + + return { size: smallSize, headerSize: 8 } +} + +function findBoxPayload(buffer, boxName, start = 0, end = buffer.byteLength) { + const view = new DataView(buffer) + let offset = start + + while (offset + 8 <= end) { + const { size, headerSize } = readBoxSize(view, offset, end) + const type = readBoxType(view, offset + 4) + + if (size < headerSize || offset + size > end) { + break + } + + const payloadStart = offset + headerSize + const payloadEnd = offset + size + + if (type === boxName) { + return buffer.slice(payloadStart, payloadEnd) + } + + if (containerBoxes.has(type)) { + const childStart = type === 'meta' ? payloadStart + 4 : payloadStart + const payload = findBoxPayload(buffer, boxName, childStart, payloadEnd) + if (payload) { + return payload + } + } + + offset += size + } + + return null +} + +function decodeUtf8(buffer) { + if (TEXT_DECODER) { + return TEXT_DECODER.decode(buffer) + } + + const bytes = new Uint8Array(buffer) + let binary = '' + + for (let i = 0; i < bytes.length; i += 1) { + binary += String.fromCharCode(bytes[i]) + } + + return decodeURIComponent(escape(binary)) +} + +function parseJsonPayload(payload) { + const rawText = decodeUtf8(payload).replace(/^\uFEFF/, '').replace(/\0+$/g, '').trim() + const jsonStart = rawText.search(/[\[{]/) + + if (jsonStart < 0) { + throw new Error('vapc box does not contain json') + } + + return JSON.parse(rawText.slice(jsonStart)) +} + +export async function getVapConfigFromMp4(mp4Url) { + if (!mp4Url) { + throw new Error('mp4 url is required') + } + + if (configCache.has(mp4Url)) { + return configCache.get(mp4Url) + } + + const configPromise = fetch(mp4Url) + .then((response) => { + if (!response.ok) { + throw new Error(`mp4 request failed: ${response.status}`) + } + + return response.arrayBuffer() + }) + .then((buffer) => { + const payload = findBoxPayload(buffer, VAP_CONFIG_BOX) + + if (!payload) { + throw new Error('vapc box not found') + } + + return parseJsonPayload(payload) + }) + .catch((error) => { + configCache.delete(mp4Url) + throw error + }) + + configCache.set(mp4Url, configPromise) + return configPromise +} + +export function preloadVapConfig(mp4Url) { + return getVapConfigFromMp4(mp4Url).catch(() => null) +} + +export function clearVapConfigCache(mp4Url) { + if (mp4Url) { + configCache.delete(mp4Url) + return + } + + configCache.clear() +}