aslan-h5/src/utils/vapMp4Config.js
2026-04-27 19:04:37 +08:00

152 lines
3.2 KiB
JavaScript

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()
}