152 lines
3.2 KiB
JavaScript
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()
|
|
}
|