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