mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-10-03 13:53:53 +02:00
Sure
This commit is contained in:
@@ -178,8 +178,13 @@ test('UrlSource with maxCacheSize: Infinity allows random access against a range
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('UrlSource in sequential mode downloads lazily and aborts the response on dispose', async () => {
|
test('UrlSource in sequential mode downloads lazily and aborts the response on dispose', async () => {
|
||||||
const fileSize = fs.statSync(videoFilePath).size;
|
// The resource is padded to a size far beyond what kernel socket buffers can swallow. This matters: the server
|
||||||
const server = await startRangelessServer();
|
// can only observe the client's laziness through backpressure, and on some systems (like Linux with its TCP
|
||||||
|
// buffer autotuning), the kernel happily buffers multiple megabytes of sent data that the client never read.
|
||||||
|
const paddingSize = 2 ** 26; // 64 MiB
|
||||||
|
const totalSize = fs.statSync(videoFilePath).size + paddingSize;
|
||||||
|
|
||||||
|
const server = await startRangelessServer({ trailingPaddingSize: paddingSize });
|
||||||
const logs = captureLogs();
|
const logs = captureLogs();
|
||||||
|
|
||||||
const input = new Input({
|
const input = new Input({
|
||||||
@@ -191,11 +196,11 @@ test('UrlSource in sequential mode downloads lazily and aborts the response on d
|
|||||||
const track = await input.getPrimaryVideoTrack();
|
const track = await input.getPrimaryVideoTrack();
|
||||||
if (!track) throw new Error('No video track found');
|
if (!track) throw new Error('No video track found');
|
||||||
|
|
||||||
// If the client were downloading eagerly, the entire file would easily arrive during this window. Instead,
|
// If the client were downloading eagerly, the entire resource would easily arrive during this window.
|
||||||
// the server is expected to stall, since data is only pulled down when reads demand it.
|
// Instead, the server is expected to stall, since data is only pulled down when reads demand it.
|
||||||
await new Promise(resolve => setTimeout(resolve, 3000));
|
await new Promise(resolve => setTimeout(resolve, 3000));
|
||||||
|
|
||||||
expect(server.bytesSent()).toBeLessThan(fileSize / 2);
|
expect(server.bytesSent()).toBeLessThan(totalSize / 2);
|
||||||
|
|
||||||
// The response is merely suspended, not dead: reading still works
|
// The response is merely suspended, not dead: reading still works
|
||||||
const sink = new EncodedPacketSink(track);
|
const sink = new EncodedPacketSink(track);
|
||||||
@@ -210,9 +215,9 @@ test('UrlSource in sequential mode downloads lazily and aborts the response on d
|
|||||||
// Give the abort a moment to propagate to the server
|
// Give the abort a moment to propagate to the server
|
||||||
await new Promise(resolve => setTimeout(resolve, 250));
|
await new Promise(resolve => setTimeout(resolve, 250));
|
||||||
|
|
||||||
// Disposal terminated the response early, long before the entire file was sent
|
// Disposal terminated the response early, long before the entire resource was sent
|
||||||
expect(server.abortedResponses()).toBe(1);
|
expect(server.abortedResponses()).toBe(1);
|
||||||
expect(server.bytesSent()).toBeLessThan(fileSize / 2);
|
expect(server.bytesSent()).toBeLessThan(totalSize / 2);
|
||||||
} finally {
|
} finally {
|
||||||
input.dispose();
|
input.dispose();
|
||||||
logs.stop();
|
logs.stop();
|
||||||
@@ -220,12 +225,11 @@ test('UrlSource in sequential mode downloads lazily and aborts the response on d
|
|||||||
}
|
}
|
||||||
}, 10_000);
|
}, 10_000);
|
||||||
|
|
||||||
/**
|
const startRangelessServer = async (
|
||||||
* Spins up a server that ignores Range headers, always responding with 200 and the full file. If responseByteLimits
|
options: { responseByteLimits?: number[]; trailingPaddingSize?: number } = {},
|
||||||
* is provided, the n-th response gets its connection killed after roughly that many bytes.
|
) => {
|
||||||
*/
|
|
||||||
const startRangelessServer = async (options: { responseByteLimits?: number[] } = {}) => {
|
|
||||||
const fileSize = fs.statSync(videoFilePath).size;
|
const fileSize = fs.statSync(videoFilePath).size;
|
||||||
|
const paddingSize = options.trailingPaddingSize ?? 0;
|
||||||
let requestCount = 0;
|
let requestCount = 0;
|
||||||
let bytesSent = 0;
|
let bytesSent = 0;
|
||||||
let abortedResponses = 0;
|
let abortedResponses = 0;
|
||||||
@@ -242,7 +246,7 @@ const startRangelessServer = async (options: { responseByteLimits?: number[] } =
|
|||||||
});
|
});
|
||||||
res.writeHead(200, {
|
res.writeHead(200, {
|
||||||
'Content-Type': 'video/mp4',
|
'Content-Type': 'video/mp4',
|
||||||
'Content-Length': fileSize,
|
'Content-Length': fileSize + paddingSize,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Stream the file with backpressure and a small chunk size, so that the amount of sent bytes closely
|
// Stream the file with backpressure and a small chunk size, so that the amount of sent bytes closely
|
||||||
@@ -267,7 +271,39 @@ const startRangelessServer = async (options: { responseByteLimits?: number[] } =
|
|||||||
res.once('drain', () => stream.resume());
|
res.once('drain', () => stream.resume());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
stream.on('end', () => res.end());
|
stream.on('end', () => {
|
||||||
|
if (paddingSize === 0) {
|
||||||
|
res.end();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append a giant 'free' box: valid ISOBMFF, but never read by the demuxer
|
||||||
|
const header = Buffer.alloc(8);
|
||||||
|
header.writeUInt32BE(paddingSize, 0);
|
||||||
|
header.write('free', 4, 'latin1');
|
||||||
|
|
||||||
|
const zeroes = Buffer.alloc(2 ** 14);
|
||||||
|
let paddingSent = 0;
|
||||||
|
|
||||||
|
const writeMore = () => {
|
||||||
|
while (paddingSent < paddingSize) {
|
||||||
|
const chunk = paddingSent === 0
|
||||||
|
? header
|
||||||
|
: zeroes.subarray(0, Math.min(paddingSize - paddingSent, zeroes.length));
|
||||||
|
|
||||||
|
paddingSent += chunk.length;
|
||||||
|
bytesSent += chunk.length;
|
||||||
|
|
||||||
|
if (!res.write(chunk)) {
|
||||||
|
res.once('drain', writeMore);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res.end();
|
||||||
|
};
|
||||||
|
writeMore();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
await new Promise<void>(resolve => server.listen(0, resolve));
|
await new Promise<void>(resolve => server.listen(0, resolve));
|
||||||
|
|||||||
Reference in New Issue
Block a user