mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-08 19:06:15 -08:00
91 lines
3.2 KiB
JavaScript
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()))
|
|
},
|
|
}
|
|
}
|