Files
ArtPlayer/test/helpers/media-gate.js
T

91 lines
3.2 KiB
JavaScript

import assert from 'node:assert/strict'
import { createHash } from 'node:crypto'
import http from 'node:http'
// Serves real media bytes, withholding the tail until a test observes native starvation.
export async function createMediaGate(bytes, contentType, limit, completeRanges = false) {
assert(Number.isSafeInteger(limit) && limit > 0 && limit < bytes.length)
const requests = []
const pending = new Map()
let released = false
const etag = `"${createHash('sha256').update(bytes).digest('hex')}"`
const server = http.createServer((req, res) => {
if (!['GET', 'HEAD'].includes(req.method) || !req.url.startsWith('/media.')) {
res.writeHead(404).end()
return
}
let start = 0
let end = bytes.length - 1
if (req.headers.range) {
const match = /^bytes=(\d*)-(\d*)$/.exec(req.headers.range)
start = match?.[1] ? Number(match[1]) : Math.max(0, bytes.length - Number(match?.[2]))
end = match?.[1] && match[2] ? Math.min(bytes.length - 1, Number(match[2])) : bytes.length - 1
if (!match || (!match[1] && !match[2]) || !Number.isSafeInteger(start) || start > end || start >= bytes.length) {
res.writeHead(416, { 'Content-Range': `bytes */${bytes.length}` }).end()
return
}
res.statusCode = 206
if (completeRanges && !released && start < limit)
end = Math.min(end, limit - 1)
res.setHeader('Content-Range', `bytes ${start}-${end}/${bytes.length}`)
}
res.setHeader('Content-Type', contentType)
res.setHeader('Content-Length', end - start + 1)
res.setHeader('Accept-Ranges', 'bytes')
res.setHeader('Cache-Control', 'no-store')
res.setHeader('ETag', etag)
res.setHeader('Access-Control-Allow-Origin', '*')
const record = { method: req.method, range: req.headers.range || null, start, end, sent: 0, wasHeld: false, closed: false }
requests.push(record)
res.on('close', () => {
record.closed = true
pending.delete(res)
})
res.on('error', (error) => {
record.error = error.message
})
if (req.method === 'HEAD') {
res.end()
return
}
const availableEnd = released ? end + 1 : Math.min(end + 1, limit)
const sentEnd = Math.max(start, availableEnd)
if (sentEnd > start) {
res.write(bytes.subarray(start, sentEnd))
record.sent += sentEnd - start
}
if (sentEnd > end) {
res.end()
}
else {
record.wasHeld = true
pending.set(res, { offset: sentEnd, end, record })
res.flushHeaders()
}
})
await new Promise((resolve, reject) => {
server.once('error', reject)
server.listen(0, '127.0.0.1', resolve)
})
const address = server.address()
return {
url: `http://127.0.0.1:${address.port}/media.${contentType === 'video/mp4' ? 'mp4' : 'm4a'}`,
requests,
get blocked() { return pending.size },
release() {
released = true
for (const [res, { offset, end, record }] of pending) {
if (!res.destroyed) {
record.sent += end + 1 - offset
res.end(bytes.subarray(offset, end + 1))
}
}
pending.clear()
},
async close() {
server.closeAllConnections()
await new Promise((resolve, reject) => server.close(error => error ? reject(error) : resolve()))
},
}
}