gallery_server_client_s190js/middleware/waitForCastSegment.js
2026-10-26 11:00:14 +01:00

298 lines
5.3 KiB
JavaScript

'use strict';
const fs = require('fs');
const path = require('path');
const SEGMENT_PATTERN =
/^segment_[0-9]{5}\.ts$/;
const DEFAULT_TIMEOUT_MS =
Number(
process.env.CAST_SEGMENT_WAIT_TIMEOUT_MS ||
300000
);
const DEFAULT_POLL_MS =
Number(
process.env.CAST_SEGMENT_WAIT_POLL_MS ||
100
);
const DEFAULT_STABILITY_MS =
Number(
process.env.CAST_SEGMENT_STABILITY_MS ||
50
);
function sleep(ms) {
return new Promise(resolve => {
setTimeout(resolve, ms);
});
}
async function getStableFileSize(
filePath,
stabilityMs
) {
try {
const first =
await fs.promises.stat(filePath);
if (
!first.isFile() ||
first.size <= 0
) {
return null;
}
await sleep(stabilityMs);
const second =
await fs.promises.stat(filePath);
if (
!second.isFile() ||
second.size <= 0 ||
second.size !== first.size
) {
return null;
}
return second.size;
} catch {
return null;
}
}
function isInsideRoot(root, candidate) {
const relative = path.relative(
root,
candidate
);
return (
relative !== '' &&
relative !== '..' &&
!relative.startsWith(`..${path.sep}`) &&
!path.isAbsolute(relative)
);
}
function requestIsClosed(req, res) {
return (
req.destroyed ||
res.destroyed ||
res.writableEnded
);
}
function createWaitForCastSegment({
castRoot,
timeoutMs = DEFAULT_TIMEOUT_MS,
pollMs = DEFAULT_POLL_MS,
stabilityMs = DEFAULT_STABILITY_MS
}) {
const resolvedRoot =
path.resolve(castRoot);
return async function waitForCastSegment(
req,
res,
next
) {
try {
if (
req.method !== 'GET' &&
req.method !== 'HEAD'
) {
next();
return;
}
let pathname;
try {
pathname = decodeURIComponent(
req.path ||
req.url.split('?')[0]
);
} catch {
res
.status(400)
.end('Invalid URL');
return;
}
const filename =
path.basename(pathname);
if (!SEGMENT_PATTERN.test(filename)) {
next();
return;
}
/*
* Il middleware è montato su /cast.
* req.path è quindi relativo a public/cast.
*/
const relativePath =
pathname.replace(/^\/+/, '');
const filePath =
path.resolve(
resolvedRoot,
relativePath
);
if (!isInsideRoot(
resolvedRoot,
filePath
)) {
res
.status(403)
.end('Forbidden');
return;
}
/*
* Se il segmento esiste già ed è stabile,
* prosegue immediatamente verso express.static.
*/
const initialSize =
await getStableFileSize(
filePath,
stabilityMs
);
if (initialSize !== null) {
next();
return;
}
const startedAt = Date.now();
const deadline =
startedAt + timeoutMs;
let lastProgressLogAt = 0;
console.log(
`[castSegmentWait] WAIT ` +
`${relativePath} ` +
`timeoutMs=${timeoutMs}`
);
while (Date.now() < deadline) {
if (requestIsClosed(req, res)) {
console.log(
`[castSegmentWait] CLIENT_CLOSED ` +
`${relativePath} ` +
`waitedMs=${Date.now() - startedAt}`
);
return;
}
const size =
await getStableFileSize(
filePath,
stabilityMs
);
if (size !== null) {
console.log(
`[castSegmentWait] READY ` +
`${relativePath} ` +
`waitedMs=${Date.now() - startedAt} ` +
`size=${size}`
);
/*
* Il file ora esiste ed è stabile.
* express.static lo invierà al client.
*/
next();
return;
}
const now = Date.now();
if (
now - lastProgressLogAt >= 5000
) {
console.log(
`[castSegmentWait] STILL_WAITING ` +
`${relativePath} ` +
`elapsedMs=${now - startedAt}`
);
lastProgressLogAt = now;
}
await sleep(pollMs);
}
if (requestIsClosed(req, res)) {
return;
}
console.warn(
`[castSegmentWait] TIMEOUT ` +
`${relativePath} ` +
`waitedMs=${Date.now() - startedAt}`
);
res
.status(503)
.set({
'Cache-Control':
'no-store',
'Retry-After':
'1'
})
.end(
'Segment not ready'
);
} catch (err) {
/*
* Chromecast può chiudere richieste speculative.
* Non è un errore di transcodifica.
*/
if (
err &&
(
err.code === 'EPIPE' ||
err.code === 'ECONNRESET'
)
) {
console.log(
`[castSegmentWait] CLIENT_DISCONNECTED ` +
`${req.originalUrl || req.url}`
);
return;
}
console.error(
`[castSegmentWait] ERROR ` +
`${req.originalUrl || req.url}: ` +
`${err.stack || err.message}`
);
if (!requestIsClosed(req, res)) {
next(err);
}
}
};
}
module.exports = {
createWaitForCastSegment
};